/*
 * Copyright Aristool AG Switzerland
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package org.opprua.tidy.beans;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.json.JSONObject;
import org.opprua.tidy.conversion.ChangerBroker;
import org.opprua.tidy.util.Primitives;
import org.opprua.tidy.util.TimePoint;

/**
 * TODO: add Java doc
 * 
 * Prepared for first commit: 15.07.2007
 * 
 * @author Lin Li
 */
public class BeanHandler {

  /**
   */
  @SuppressWarnings("unchecked")
  public static <T> T stuff(Class<T> c) {
    if (c.isArray()) {
      return (T) Array.newInstance(c.getComponentType(), 0);
    }

    T target = ChangerBroker.getDefaultValue(c);
    if (target != null) {
      return target;
    }

    try {
      target = c.newInstance();
    } catch (Throwable t) {
      throw new Error("Can not create object of class: " + c.getName(), t);
    }

    BeanField[] fields = BeanField.getFields(c).values().toArray(
                new BeanField[0]);
    for (int i = 0; i < fields.length; ++i) {
      fields[i].set(target, stuff(fields[i].getType()));
    }
    return target;
  }

  /**
   */
  @SuppressWarnings("unchecked")
  public static <T> T collect(Class<T> type, Object raw) {
    if (raw == null) {
      return stuff(type);
    }

    if (raw.getClass().isArray()) {
      Class<?> ttype = type;
      if (ttype.isArray()) {
        ttype = type.getComponentType();
      }
      Object[] res = (Object[]) Array.newInstance(ttype, Array.getLength(raw));
      for (int i = 0; i < res.length; i++) {
        res[i] = collect(ttype, Array.get(raw, i));
      }
      return (T) res;
    }

    try {
      T res = stuff(type);
      BeanCoupler.getFor(raw.getClass(), type).copy(raw, res);
      return res;
    } catch (Throwable t) {
      throw new Error("failed to create new object with " + type, t);
    }
  }

  /**
   */
  public static int copy(Object sData, Object tData) {
    if (sData == null || tData == null) return 0;
    return BeanCoupler.getFor(sData.getClass(), tData.getClass()).copy(sData,
                tData);
  }

  /**
   */
  public static Object duplicate(Object source) {
    if (source.getClass().isArray()) {
      Class<?> t = source.getClass().getComponentType();
      Object o = Array.newInstance(t, Array.getLength(source));
      for (int i = 0; i < Array.getLength(source); i++) {
        Array.set(o, i, duplicate(Array.get(source, i)));
      }
      return o;
    }
    Map<String, BeanField> m = BeanField.getFields(source.getClass());
    if (m.size() == 0) {
      return source;
    }
    Object o = stuff(source.getClass());
    for (Iterator<String> iter = m.keySet().iterator(); iter.hasNext();) {
      String k = iter.next();
      BeanField g = (BeanField) m.get(k);
      g.set(o, duplicate(g.get(source)));
    }
    return o;
  }

  /**
   */
  public static boolean equals(Object sData, Object tData, boolean mixedList) {

    if (sData.equals(tData)) {
      return true;
    }
    if (sData.getClass().equals(tData.getClass())) {
      try {
        Method m = sData.getClass()
                    .getDeclaredMethod("compareTo", Object.class);
        if (!m.getClass().equals(Object.class)) return false;
      } catch (Throwable tx) {}
    }

    Class<?> stype = sData.getClass();
    Class<?> ttype = tData.getClass();

    if (stype.isArray()) {
      if (!ttype.isArray()) {
        return false;
      }
      if (Array.getLength(sData) != Array.getLength(tData)) {
        return false;
      }
      List<Object> s = Arrays.asList((Object[]) sData);
      List<Object> t = Arrays.asList((Object[]) tData);
      for (int i = 0; i < s.size(); i++) {
        if (mixedList) {
          boolean found = false;
          for (int j = 0; j < t.size(); j++) {
            if (t.get(j) != null && equals(s.get(i), t.get(j), mixedList)) {
              t.set(j, null);
              found = true;
              break;
            }
          }
          if (!found) {
            return false;
          }
        } else {
          if (!equals(s.get(i), t.get(i), mixedList)) {
            return false;
          }
        }
      }
      return true;
    }

    if (Date.class.isAssignableFrom(stype)
                && Date.class.isAssignableFrom(ttype)) {
      TimePoint tt = TimePoint.from(tData);
      TimePoint ts = TimePoint.from(sData);
      return tt.getTime() == ts.getTime();
    }

    Map<String, BeanField> osource = BeanField.getFields(stype);
    Map<String, BeanField> otarget = BeanField.getFields(ttype);

    if (!osource.keySet().equals(otarget.keySet())
                || osource.keySet().size() == 0) {
      return false;
    }

    for (Iterator<String> iter = osource.keySet().iterator(); iter.hasNext();) {
      Object key = (Object) iter.next();
      if (!equals(((BeanField) osource.get(key)).get(sData),
                  ((BeanField) otarget.get(key)).get(tData), mixedList)) {
        return false;
      }
    }
    return true;
  }

  /**
   */
  public static void setNull(Object o, String path) {
    Object c = o;
    Object[] pa = BeanField.getPath(o.getClass(), path);
    for (int i = 0; i < pa.length - 1; i++) {
      c = ((BeanField) pa[i]).get(c);
    }
    Object p = pa[pa.length - 1];
    if (p instanceof BeanField) {
      ((BeanField) p).setNull(c);
    } else {
      if (pa.length == 1) {
        throw new Error("Can not set null with path " + p + " on a "
                    + c.getClass());
      } else {
        setNull(c, (String) p);
      }
    }
  }

  /**
   */
  public static void set(Object o, String path, Object value) {
    BeanField.set(o, BeanField.getPath(o.getClass(), path), value);
  }

  /**
   */
  public static Object get(Object o, String path) {
    return BeanField.get(o, BeanField.getPath(o.getClass(), path));
  }

  /**
   */
  public static <T> T fake(Class<T> c) {
    return BeanFaker.fake(c, "");
  }

  /**
   */
  @SuppressWarnings("unchecked")
  public static <T, S> Map<T, S> mapIt(String field, Class<T> type, S[] data) {
    Map<T, S> m = new TreeMap<T, S>();
    for (int i = 0; i < data.length; i++) {
      Object[] path = BeanField.getPath(data[i].getClass(), field);
      m.put((T) BeanField.get(data[i], path), data[i]);
    }
    return m;
  }

  /**
   */
  @SuppressWarnings("unchecked")
  public static <T, S> Map<T, List<S>> mapThem(String field, Class<T> type,
              S[] data) {
    Map<T, List<S>> m = new TreeMap<T, List<S>>();
    for (int i = 0; i < data.length; i++) {
      Object[] path = BeanField.getPath(data[i].getClass(), field);
      T o = (T) BeanField.get(data[i], path);

      List<S> l = m.get(o);
      if (l == null) {
        l = new ArrayList<S>();
        m.put(o, l);
      }
      l.add(data[i]);
    }
    return m;
  }

  public static Class<?> getClass(String name) {
    try {
      return Primitives.getFor(name).getTypeClass();
    } catch (Throwable e) {}

    try {
      return Class.forName(name, true, Thread.currentThread()
                  .getContextClassLoader());
    } catch (Throwable e) {}

    try {
      return Class.forName(name, true, ClassLoader.getSystemClassLoader());
    } catch (Throwable e) {}

    try {
      return Class.forName(name, true, BeanHandler.class.getClassLoader());
    } catch (Throwable e) {
      return null;
    }
  }

  public static String toJSON(Object o) {
    return BeanJson.toJSON(o);
  }

  public static <T> T fromJSON(Class<T> type, String json) {
    return BeanJson.fromJSON(type, json);
  }

  public static <T> T fromJSON(Class<T> type, JSONObject json) {
    return BeanJson.from(type, json);
  }

  public static String toXML(Object o) {
    return BeanXMLStreamer.stream(o);
  }

  public static <T> T fromXML(Class<T> type, String xml) {
    return BeanXMLSaxParser.toObject(type, xml);
  }
}
